package k3s

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
	"io"
	"io/ioutil"
	"net"
	"net/url"
	"time"
	"v8s/logger"

	"github.com/docker/go-connections/nat"

	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/clientcmd/api"
)

type (
	Container struct {
		testcontainers.Container
	}
)

const (
	k3sDockerImage                 = "rancher/k3s:v1.21.3-k3s1"
	k3sApiPort                     = "6443"
	k3sConfigPath                  = "/etc/rancher/k3s/k3s.yaml"
	defaultContainerStartupTimeout = time.Minute * 5
)

func StartContainer(ctx context.Context) *Container {
	binded := map[string]string{}
	binded["/sys/fs/cgroup"] = "/sys/fs/cgroup"

	cmd := []string{"server", "--no-deploy=traefik", "--tls-san=" + getHostAddress()}

	request := testcontainers.ContainerRequest{
		Image:        k3sDockerImage,
		ExposedPorts: []string{k3sApiPort},
		Cmd:          cmd,
		Mounts: []testcontainers.ContainerMount{{
			Source: testcontainers.DockerBindMountSource{HostPath: "/sys/fs/cgroup"},
			Target: "/sys/fs/cgroup",
		}}, // +
		WaitingFor: wait.ForLog("Node controller sync successful").
			WithStartupTimeout(TimeoutFromContext(ctx)),
		Privileged: true, // +
		AutoRemove: true,
	}

	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: request,
		Started:          true,
	})
	if err != nil {
		panic(err)
	}

	go func() {
		log := logger.Logger().Named("k3s-container").Sugar()
		reader, err := container.Logs(ctx)
		if err != nil {
			log.Errorf("failed to obtain log reader: %s", err)
			return
		}

		buffered := bufio.NewReader(reader)

		for {
			line, _, err := buffered.ReadLine()
			if errors.Is(err, io.EOF) {
				continue
			}

			if err != nil {
				log.Errorf("failed to read line from container log: %s", err)
				break
			}

			log.Info(string(line))
		}

	}()

	return &Container{
		Container: container,
	}
}

func TimeoutFromContext(ctx context.Context) time.Duration {
	timeout := defaultContainerStartupTimeout

	if deadline, ok := ctx.Deadline(); ok {
		timeout = deadline.Sub(time.Now())
	}

	return timeout
}

func (c *Container) GetClient(ctx context.Context) *kubernetes.Clientset {
	kubeconfig, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*api.Config, error) {
		reader, err := c.CopyFileFromContainer(ctx, k3sConfigPath)
		if err != nil {
			return nil, fmt.Errorf("failed to copy file from container: %w", err)
		}

		data, err := ioutil.ReadAll(reader)
		if err != nil {
			return nil, fmt.Errorf("failed to read data from reader: %w", err)
		}
		config, err := clientcmd.Load(data)
		if err != nil {
			return nil, fmt.Errorf("failed to load config from data: %w", err)
		}

		for name, cluster := range config.Clusters {
			parsed, err := url.Parse(cluster.Server)
			if err != nil {
				return nil, fmt.Errorf("failed to parse url '%s' for cluster '%s': %w", cluster.Server, name, err)
			}

			port := parsed.Port()
			natPort, err := nat.NewPort("", port)
			if err != nil {
				return nil, fmt.Errorf("failed to parse port '%s': %w", port, err)
			}

			mappedPort, err := c.Container.MappedPort(context.Background(), natPort)
			if err != nil {
				return nil, fmt.Errorf("failed to obtain mapper port '%d': %w", natPort.Int(), err)
			}

			patchedUrl, err := url.Parse(fmt.Sprintf("%s://%s:%d", parsed.Scheme, parsed.Hostname(), mappedPort.Int()))
			if err != nil {
				return nil, fmt.Errorf("failed to construct url: %w", err)
			}

			cluster.Server = patchedUrl.String()
		}

		return config, nil
	})
	if err != nil {
		panic(fmt.Errorf("failed to create client: %w", err))
	}

	return kubernetes.NewForConfigOrDie(kubeconfig)
}

func getHostAddress() string {
	conn, err := net.Dial("udp", "8.8.8.8:80")
	if err != nil {
		panic(fmt.Sprintf("failed to dial to 8.8.8.8:80: %s", err))
	}
	defer func() {
		_ = conn.Close()
	}()

	localAddr := conn.LocalAddr().(*net.UDPAddr)

	return localAddr.IP.String()
}
